package com.putao.tinytime.event;

import android.os.Looper;
import android.util.Log;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class EventBus {
	static ExecutorService executorService = Executors.newCachedThreadPool();
	public static String TAG = "Event";
	private static volatile EventBus defaultInstance;
	private static final Map<Class<?>, List<Class<?>>> eventTypesCache = new HashMap<Class<?>, List<Class<?>>>();
	private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
	private final Map<Object, List<Class<?>>> typesBySubscriber;
	private final Map<Class<?>, Object> stickyEvents;
	private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
		@Override
		protected PostingThreadState initialValue() {
			return new PostingThreadState();
		}
	};
	private String defaultMethodName = "onEvent";
	private final HandlerPoster mainThreadPoster;
	private final BackgroundPoster backgroundPoster;
	private final AsyncPoster asyncPoster;
	private final SubscriberMethodFinder subscriberMethodFinder;
	private boolean subscribed;

	public static EventBus getEventBus() {
		if (defaultInstance == null) {
			synchronized (EventBus.class) {
				if (defaultInstance == null) {
					defaultInstance = new EventBus();
				}
			}
		}
		return defaultInstance;
	}

	/** For unit test primarily. */
	public static void clearCaches() {
		SubscriberMethodFinder.clearCaches();
		eventTypesCache.clear();
	}

	/**
	 * Method name verification is done for methods starting with onEvent to
	 * avoid typos; using this method you can exclude subscriber classes from
	 * this check. Also disables checks for method modifiers (public, not static
	 * nor abstract).
	 */
	public static void skipMethodVerificationFor(Class<?> clazz) {
		SubscriberMethodFinder.skipMethodVerificationFor(clazz);
	}

	/** For unit test primarily. */
	public static void clearSkipMethodNameVerifications() {
		SubscriberMethodFinder.clearSkipMethodVerifications();
	}

	/**
	 * Creates a new EventBus instance; each instance is a separate scope in
	 * which events are delivered. To use a central bus, consider
	 * {@link #getDefault()}.
	 */
	public EventBus() {
		subscriptionsByEventType = new HashMap<Class<?>, CopyOnWriteArrayList<Subscription>>();
		typesBySubscriber = new HashMap<Object, List<Class<?>>>();
		stickyEvents = new ConcurrentHashMap<Class<?>, Object>();
		mainThreadPoster = new HandlerPoster(this, Looper.getMainLooper(), 10);
		backgroundPoster = new BackgroundPoster(this);
		asyncPoster = new AsyncPoster(this);
		subscriberMethodFinder = new SubscriberMethodFinder();
	}

	/**
	 * Before registering any subscribers, use this method to configure if
	 * EventBus should log exceptions thrown by subscribers (default: true).
	 */
	public void configureLogSubscriberExceptions(boolean logSubscriberExceptions) {
		if (subscribed) {
			throw new EventBusException(
					"This method must be called before any registration");
		}
	}

	/**
	 * Registers the given subscriber to receive events. Subscribers must call
	 * {@link #unregister(Object)} once they are no longer interested in
	 * receiving events.
	 * 
	 * Subscribers have event handling methods that are identified by their
	 * name, typically called "onEvent". Event handling methods must have
	 * exactly one parameter, the event. If the event handling method is to be
	 * called in a specific thread, a modifier is appended to the method name.
	 * Valid modifiers match one of the {@link ThreadMode} enums. For example,
	 * if a method is to be called in the UI/main thread by EventBus, it would
	 * be called "onEventMainThread".
	 */
	public void register(Object subscriber) {
		register(subscriber, defaultMethodName, false, 0);
	}

	/**
	 * Like {@link #register(Object)} with an additional subscriber priority to
	 * influence the order of event delivery. Within the same delivery thread (
	 * {@link ThreadMode}), higher priority subscribers will receive events
	 * before others with a lower priority. The default priority is 0. Note: the
	 * priority does *NOT* affect the order of delivery among subscribers with
	 * different {@link ThreadMode}s!
	 */
	public void register(Object subscriber, int priority) {
		register(subscriber, defaultMethodName, false, priority);
	}

	/**
	 * Like {@link #register(Object)}, but also triggers delivery of the most
	 * recent sticky event (posted with {@link #postSticky(Object)}) to the
	 * given subscriber.
	 */
	public void registerSticky(Object subscriber) {
		register(subscriber, defaultMethodName, true, 0);
	}

	/**
	 * Like {@link #register(Object,int)}, but also triggers delivery of the
	 * most recent sticky event (posted with {@link #postSticky(Object)}) to the
	 * given subscriber.
	 */
	public void registerSticky(Object subscriber, int priority) {
		register(subscriber, defaultMethodName, true, priority);
	}

	private synchronized void register(Object subscriber, String methodName,
			boolean sticky, int priority) {
		List<SubscriberMethod> subscriberMethods = subscriberMethodFinder
				.findSubscriberMethods(subscriber.getClass(), methodName);
		for (SubscriberMethod subscriberMethod : subscriberMethods) {
			subscribe(subscriber, subscriberMethod, sticky, priority);
		}
	}

	// Must be called in synchronized block
	private void subscribe(Object subscriber,
			SubscriberMethod subscriberMethod, boolean sticky, int priority) {
		subscribed = true;
		Class<?> eventType = subscriberMethod.eventType;
		CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType
				.get(eventType);
		Subscription newSubscription = new Subscription(subscriber,
				subscriberMethod, priority);
		if (subscriptions == null) {
			subscriptions = new CopyOnWriteArrayList<Subscription>();
			subscriptionsByEventType.put(eventType, subscriptions);
		} else {
			for (Subscription subscription : subscriptions) {
				if (subscription.equals(newSubscription)) {
					throw new EventBusException("Subscriber "
							+ subscriber.getClass()
							+ " already registered to event " + eventType);
				}
			}
		}
		// Starting with EventBus 2.2 we enforced methods to be public (might
		// change with annotations again)
		// subscriberMethod.method.setAccessible(true);
		int size = subscriptions.size();
		for (int i = 0; i <= size; i++) {
			if (i == size
					|| newSubscription.priority > subscriptions.get(i).priority) {
				subscriptions.add(i, newSubscription);
				break;
			}
		}
		List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
		if (subscribedEvents == null) {
			subscribedEvents = new ArrayList<Class<?>>();
			typesBySubscriber.put(subscriber, subscribedEvents);
		}
		subscribedEvents.add(eventType);
		if (sticky) {
			Object stickyEvent;
			synchronized (stickyEvents) {
				stickyEvent = stickyEvents.get(eventType);
			}
			if (stickyEvent != null) {
				// If the subscriber is trying to abort the event, it will fail
				// (event is not tracked in posting state)
				// --> Strange corner case, which we don't take care of here.
				postToSubscription(newSubscription, stickyEvent,
						Looper.getMainLooper() == Looper.myLooper());
			}
		}
	}

	public synchronized boolean isRegistered(Object subscriber) {
		return typesBySubscriber.containsKey(subscriber);
	}

	/**
	 * Only updates subscriptionsByEventType, not typesBySubscriber! Caller must
	 * update typesBySubscriber.
	 */
	private void unubscribeByEventType(Object subscriber, Class<?> eventType) {
		List<Subscription> subscriptions = subscriptionsByEventType
				.get(eventType);
		if (subscriptions != null) {
			int size = subscriptions.size();
			for (int i = 0; i < size; i++) {
				Subscription subscription = subscriptions.get(i);
				if (subscription.subscriber == subscriber) {
					subscription.active = false;
					subscriptions.remove(i);
					i--;
					size--;
				}
			}
		}
	}

	/** Unregisters the given subscriber from all event classes. */
	public synchronized void unregister(Object subscriber) {
		List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
		if (subscribedTypes != null) {
			for (Class<?> eventType : subscribedTypes) {
				unubscribeByEventType(subscriber, eventType);
			}
			typesBySubscriber.remove(subscriber);
		} else {
			Log.w(TAG, "Subscriber to unregister was not registered before: "
                    + subscriber.getClass());
		}
	}

	/** Posts the given event to the event bus. */
	public void post(Object event) {
		PostingThreadState postingState = currentPostingThreadState.get();
		List<Object> eventQueue = postingState.eventQueue;
		eventQueue.add(event);
		if (postingState.isPosting) {
			return;
		} else {
			postingState.isMainThread = Looper.getMainLooper() == Looper
					.myLooper();
			postingState.isPosting = true;
			if (postingState.canceled) {
				throw new EventBusException(
						"Internal error. Abort state was not reset");
			}
			try {
				while (!eventQueue.isEmpty()) {
					postSingleEvent(eventQueue.remove(0), postingState);
				}
			} finally {
				postingState.isPosting = false;
				postingState.isMainThread = false;
			}
		}
	}

	/**
	 * Called from a subscriber's event handling method, further event delivery
	 * will be canceled. Subsequent subscribers won't receive the event. Events
	 * are usually canceled by higher priority subscribers (see
	 * {@link #register(Object, int)}). Canceling is restricted to event
	 * handling methods running in posting thread {@link ThreadMode#PostThread}.
	 */
	public void cancelEventDelivery(Object event) {
		PostingThreadState postingState = currentPostingThreadState.get();
		if (!postingState.isPosting) {
			throw new EventBusException(
					"This method may only be called from inside event handling methods on the posting thread");
		} else if (event == null) {
			throw new EventBusException("Event may not be null");
		} else if (postingState.event != event) {
			throw new EventBusException(
					"Only the currently handled event may be aborted");
		} else if (postingState.subscription.subscriberMethod.threadMode != ThreadMode.PostThread) {
			throw new EventBusException(
					" event handlers may only abort the incoming event");
		}
		postingState.canceled = true;
	}

	/**
	 * Posts the given event to the event bus and holds on to the event (because
	 * it is sticky). The most recent sticky event of an event's type is kept in
	 * memory for future access. This can be {@link #registerSticky(Object)} or
	 * {@link #getStickyEvent(Class)}.
	 */
	public void postSticky(Object event) {
		synchronized (stickyEvents) {
			stickyEvents.put(event.getClass(), event);
		}
		// Should be posted after it is putted, in case the subscriber wants to
		// remove immediately
		post(event);
	}

	/**
	 * Gets the most recent sticky event for the given type.
	 * 
	 * @see #postSticky(Object)
	 */
	public <T> T getStickyEvent(Class<T> eventType) {
		synchronized (stickyEvents) {
			return eventType.cast(stickyEvents.get(eventType));
		}
	}

	/**
	 * Remove and gets the recent sticky event for the given event type.
	 * 
	 * @see #postSticky(Object)
	 */
	public <T> T removeStickyEvent(Class<T> eventType) {
		synchronized (stickyEvents) {
			return eventType.cast(stickyEvents.remove(eventType));
		}
	}

	/**
	 * Removes the sticky event if it equals to the given event.
	 * 
	 * @return true if the events matched and the sticky event was removed.
	 */
	public boolean removeStickyEvent(Object event) {
		synchronized (stickyEvents) {
			Class<? extends Object> eventType = event.getClass();
			Object existingEvent = stickyEvents.get(eventType);
			if (event.equals(existingEvent)) {
				stickyEvents.remove(eventType);
				return true;
			} else {
				return false;
			}
		}
	}

	/**
	 * Removes all sticky events.
	 */
	public void removeAllStickyEvents() {
		synchronized (stickyEvents) {
			stickyEvents.clear();
		}
	}

	private void postSingleEvent(Object event, PostingThreadState postingState)
			throws Error {
		Class<? extends Object> eventClass = event.getClass();
		List<Class<?>> eventTypes = findEventTypes(eventClass);
		int countTypes = eventTypes.size();
		for (int h = 0; h < countTypes; h++) {
			Class<?> clazz = eventTypes.get(h);
			CopyOnWriteArrayList<Subscription> subscriptions;
			synchronized (this) {
				subscriptions = subscriptionsByEventType.get(clazz);
			}
			if (subscriptions != null && !subscriptions.isEmpty()) {
				for (Subscription subscription : subscriptions) {
					postingState.event = event;
					postingState.subscription = subscription;
					boolean aborted = false;
					try {
						postToSubscription(subscription, event,
								postingState.isMainThread);
						aborted = postingState.canceled;
					} finally {
						postingState.event = null;
						postingState.subscription = null;
						postingState.canceled = false;
					}
					if (aborted) {
						break;
					}
				}
			}
		}
	}

	private void postToSubscription(Subscription subscription, Object event,
			boolean isMainThread) {
		switch (subscription.subscriberMethod.threadMode) {
		case PostThread:
			invokeSubscriber(subscription, event);
			break;
		case MainThread:
			if (isMainThread) {
				invokeSubscriber(subscription, event);
			} else {
				mainThreadPoster.enqueue(subscription, event);
			}
			break;
		case BackgroundThread:
			if (isMainThread) {
				backgroundPoster.enqueue(subscription, event);
			} else {
				invokeSubscriber(subscription, event);
			}
			break;
		case Async:
			asyncPoster.enqueue(subscription, event);
			break;
		default:
			throw new IllegalStateException("Unknown thread mode: "
					+ subscription.subscriberMethod.threadMode);
		}
	}

	/** Finds all Class objects including super classes and interfaces. */
	private List<Class<?>> findEventTypes(Class<?> eventClass) {
		synchronized (eventTypesCache) {
			List<Class<?>> eventTypes = eventTypesCache.get(eventClass);
			if (eventTypes == null) {
				eventTypes = new ArrayList<Class<?>>();
				Class<?> clazz = eventClass;
				while (clazz != null) {
					eventTypes.add(clazz);
					addInterfaces(eventTypes, clazz.getInterfaces());
					clazz = clazz.getSuperclass();
				}
				eventTypesCache.put(eventClass, eventTypes);
			}
			return eventTypes;
		}
	}

	/** Recurses through super interfaces. */
	static void addInterfaces(List<Class<?>> eventTypes, Class<?>[] interfaces) {
		for (Class<?> interfaceClass : interfaces) {
			if (!eventTypes.contains(interfaceClass)) {
				eventTypes.add(interfaceClass);
				addInterfaces(eventTypes, interfaceClass.getInterfaces());
			}
		}
	}

	/**
	 * Invokes the subscriber if the subscriptions is still active. Skipping
	 * subscriptions prevents race conditions between
	 * {@link #unregister(Object)} and event delivery. Otherwise the event might
	 * be delivered after the subscriber unregistered. This is particularly
	 * important for main thread delivery and registrations bound to the live
	 * cycle of an Activity or Fragment.
	 */
	void invokeSubscriber(PendingPost pendingPost) {
		Object event = pendingPost.event;
		Subscription subscription = pendingPost.subscription;
		PendingPost.releasePendingPost(pendingPost);
		if (subscription.active) {
			invokeSubscriber(subscription, event);
		}
	}

	void invokeSubscriber(Subscription subscription, Object event) throws Error {
		try {
			subscription.subscriberMethod.method.invoke(
					subscription.subscriber, event);
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			throw new IllegalStateException("Unexpected exception", e);
		}
	}

	/** For ThreadLocal, much faster to set (and get multiple values). */
	final static class PostingThreadState {
		List<Object> eventQueue = new ArrayList<Object>();
		boolean isPosting;
		boolean isMainThread;
		Subscription subscription;
		Object event;
		boolean canceled;
	}
}
